package Server;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;

import Common.Base64;
import Common.Constants;
import Common.Utils;

import com.sun.net.httpserver.HttpExchange;

public class myServerService implements ServerService{

	// GLOBAL VARS.

	// local storage dir.
	static String BULib;

	// listing object
	static Listing listing;

	// the listing's list of files
	public static Map<String, ListingEntry> fileList;
	public static List<String> fileListObjectsToClean;

	// a refCount list
	public static RefList refList;

	public void init(String initBULib){
		BULib = initBULib;
		//make sure the backup library exists
		File dir = new File(BULib);
		if (!dir.exists()){		
			System.out.println(BULib + " does not exist.");
			System.out.println("creating " + BULib);
			dir.mkdirs();
		}

		//retrieve the listing.dat file
		try {
			listing = Listing.readListing(BULib + ServerUtils.LISTING_FILE_NAME);
		} catch (FileNotFoundException e) {
			System.out.println("listing file in the library: " + BULib +  " not found."); 
			System.out.println("creating a new listing file for current directory.");
			listing = new Listing();
		}
		fileList = listing.getListOfFiles();
		fileListObjectsToClean = new ArrayList<String>();
		refList = new RefList();

		// CONSISTENCY CHECK (see documentation)
		System.out.println("Checking data consistency");
		if (!ServerUtils.consistCheck(listing, refList, BULib)){
			System.out.println("the listing file is not consistent with the directory.");
			// might have made some changes to the listing - update changes to file
			try {
				Listing.writeListing(BULib + ServerUtils.LISTING_FILE_NAME, listing);
			} catch (FileNotFoundException e) {
				e.printStackTrace();
				System.out.println("Error writing to listings file.");
				return;
			}
		}
		else {
			System.out.println("consistency check done.");
			System.out.println("loaded listing.dat from " + BULib);
		}
	}

	public void restoreFile(String userName,String fileName, int snapshot, HttpExchange exchange){
		if (filenameExists(fileName, userName, snapshot)){
			ServerUtils.debugMsg("User: " + userName + " wants to restore file: " + fileName + " from snapshot " + snapshot);
			ListingEntry entry = getEntry(fileName, userName, snapshot);
			if (entry != null){		// the file is in the listing and belongs to userName
				ServerUtils.sendFileResponse(exchange, entry.getLocalName(), BULib);
	
			}
			else {		// the file is not in the listing
				System.out.println("Input data file: " + fileName + " is not in the listing");
				ServerUtils.sendFileNotFoundMsg(exchange);
			}
		}
		else {
			ServerUtils.sendFileNotFoundMsg(exchange);
		}
	}

	public void listDir(String fileName, String userName, int snapshot, HttpExchange exchange) {
		List<ListingEntry> ls = ServerUtils.lsDir(fileName, listing, userName, snapshot);
		if (ls == null){		//dir not found
			ServerUtils.sendFileNotFoundMsg(exchange);
		}
		else{		// send back the list
			ServerUtils.sendLSResponse(exchange, ls);
		}
	}

	public void getSignature(String fileName, String userName,
			HttpExchange exchange) {
		//get the requested listing		DEBUG: it wasn't defined to which snapshot the signature req. should relate - used current
		ListingEntry entry = getEntry(fileName, userName, Constants.CURRENT_SNAPSHOT);
		if (entry == null) {
			System.out.println("Input data file " + fileName + " does not exist for user: " + userName + ".");
			ServerUtils.sendFileNotFoundMsg(exchange);
		}
		else { 
			ServerUtils.sendSignatureResponse(exchange, entry.getFileHash());
		}

	}

	public void check(String fileName, String userName, HttpExchange exchange) {
		if (!filenameExists(fileName, userName, Constants.CURRENT_SNAPSHOT)){
			List<ListingEntry> ls = ServerUtils.lsDir(fileName, listing, userName, Constants.UNSPEC_SNAPSHOT);
			if (ls == null){
				ServerUtils.sendFileNotFoundMsg(exchange);
			}
			else{
				sendCheckResponse(exchange, ls, userName);
			}
		}
	}

	public void createSnapshot(HttpExchange exchange, String userName, int snapshotID){
		if (snapshotID != 0){
			//now go over all user files from working copy , add  snapshotID to snapshots[]
			if(createSnapshotPointers(userName,snapshotID)){
				if (updateListing()){
					// send back ok to the client
					ServerUtils.sendOKMsg(exchange);
				}
				else { 
					ServerUtils.sendInternErrMsg(exchange);
				}
			}
			else{
				ServerUtils.sendBadRequestMsg(exchange);
			}
		}
		else{
			ServerUtils.sendBadRequestMsg(exchange);
		}
	}
	
	public void backupWithSignature(HttpExchange exchange, String userName, String fileName, byte[] signature){
		

		String localFilename = getLocalFileByHash(signature);
		if (localFilename != null){	//the signature exists - use existing local file  
			//(------DEDUPLICATION-----)
			//String newFilename = userName + Constants.FILENAME_SEPERATOR + localFilename;

			String newFilename = userName + Constants.FILENAME_SEPERATOR + fileName + 
			Constants.FILENAME_SEPERATOR + localFilename;

			ListingEntry entry = fileList.get(newFilename);
			if (entry != null){// in case we already have exact file on disk for user
				if (entry.isInSnapshot(Constants.CURRENT_SNAPSHOT)){	// already in current snapshot
					//we have a copy of this file with the same name for this user: nothing to do
					ServerUtils.debugMsg("User : " + userName + " already has this backup for File: " + fileName + ".");
					ServerUtils.sendOKMsg(exchange);
				}
				else 
				{	// this entry is right but not in current snapshot

					// try to find a listing to update
					ListingEntry prevEntry = getEntry(fileName, userName, Constants.CURRENT_SNAPSHOT);
					if (prevEntry != null){	//found a previous entry
						// remove current snapshot mark from the old entry and remove ref-count
						ServerUtils.removeFromCurrentSnapshot(prevEntry);
						refList.remCount(prevEntry.getLocalName());
						// remove entries from listing and refList if needed
						deleteEntryOnLastSnapshot(prevEntry, true);
						deleteOnLastRef(prevEntry.getLocalName());
					}

					// mark the entry for current snapshot and add ref-count for the local file
					ServerUtils.addToCurrentSnapshot(entry);
					refList.addCount(entry.getLocalName());

					// update listing file
					if (updateListing()){
						// send back ok to the client
						System.out.println("File: " + fileName + " successfuly stored.");
						ServerUtils.sendOKMsg(exchange);
					}
					else {
						ServerUtils.sendInternErrMsg(exchange);
					}
				}
			}
			else {//maybe there is a version of this file for this user which at the moment contains snapshot ID '0'
				//in that case we need to create a new entry with new hash and and transfer the ID '0' there

				// a new entry for this filename and user
				// add the entry

				//find previous entry with '0'
				ListingEntry prevEntry = getEntry(fileName, userName, Constants.CURRENT_SNAPSHOT);
				if (prevEntry != null){
					ServerUtils.debugMsg("found old entry for the file");
					// remove current snapshot mark from the old entry and remove ref-count
					ServerUtils.removeFromCurrentSnapshot(prevEntry);
					// remove listing entry if it is not in any snapshot
					deleteEntryOnLastSnapshot(prevEntry,true);
					refList.remCount(prevEntry.getLocalName());
					deleteOnLastRef(prevEntry.getLocalName());
				}

				//create new entry marked with current snapshot
				int[] snapshotVersion = {Constants.CURRENT_SNAPSHOT};
				ListingEntry newEntry = new ListingEntry(fileName, localFilename, signature, userName, snapshotVersion);
				fileList.put(newFilename, newEntry);

				// mark the entry for current snapshot and add ref-count for the local file
				refList.addCount(newEntry.getLocalName());

				// update listing
				if (updateListing()){
					// send back ok to the client
					System.out.println("File: " + fileName + " successfuly stored.");
					ServerUtils.sendOKMsg(exchange);
				}
				else {
					ServerUtils.sendInternErrMsg(exchange);
				}
			}
		}
		else {	//signature doesn't exist
			ServerUtils.sendFileNotFoundMsg(exchange);
		}
	}


	public void backupWithData(HttpExchange exchange, String userName, String fileName){
		
		// retrieve data from request
		byte[] inputFileInBytes = null;
		byte[] fileHash = null;
		try {		//read actual data
			inputFileInBytes = Utils.readInputStream(exchange.getRequestBody());
			fileHash = Utils.getSHA256hash(inputFileInBytes);
		} catch (IOException e) {
			e.printStackTrace();
			System.out.println("Error reading data from request body.");
			ServerUtils.sendInternErrMsg(exchange);
			return;
		}

		// give the file a local filename
		String localFilename = null;
		try {
			localFilename = ServerUtils.convertToLocalFilename(fileHash);

		} catch (IOException e1) {
			e1.printStackTrace();
			System.out.println("IO Exception occured when tried to convert to local file-name.");
			return;
		}
		// add local filename to refList
		refList.addEntry(localFilename);
		// set key for listing
		String newFilename = userName + Constants.FILENAME_SEPERATOR + fileName + Constants.FILENAME_SEPERATOR + localFilename;

		// handle refCount for the file
		ListingEntry entry = getEntry(fileName, userName, Constants.CURRENT_SNAPSHOT);
		if (entry == null) {	// file doesn't exists - this is a new file backup for this filename and user
			ServerUtils.debugMsg("file: " + fileName + " for user: " + userName + " is new");
			// add a new entry in the listing for the received file
			int[] snapshotVersion = {Constants.CURRENT_SNAPSHOT};
			fileList.put(newFilename , new ListingEntry(fileName, localFilename, fileHash, userName, snapshotVersion));
		}
		else {	// the file exists and has 1+ references
			// remove a ref-count from the previous local file
			refList.remCount(entry.getLocalName());
			deleteOnLastRef(entry.getLocalName());

			// check if this entry is for other snapshots too
			if (entry.getSnapshotVersion().length > 1){	// other snapshots use this entry - create a new entry
				ServerUtils.debugMsg("file: " + fileName + " is in an entry that older snapshot use");
				//remove the current snapshot mark from the entry
				ServerUtils.removeFromCurrentSnapshot(entry);
				// add a new entry for the filename, user, hash under the current snapshot
				int[] snapshotVersion = {Constants.CURRENT_SNAPSHOT};
				ListingEntry newEntry = new ListingEntry(fileName, localFilename, fileHash, userName, snapshotVersion);
				fileList.put(newFilename, newEntry);

			}
			else {	// this entry is for the current snapshot only - reuse it
				ServerUtils.debugMsg("this is a backup for a file that's listed in one entry with no old snapshots");
				ServerUtils.debugMsg("reusing listing entry for the backup");
				//remove old map entry
				String oldKey = entry.getOwner() + Constants.FILENAME_SEPERATOR + entry.getFilename() + Constants.FILENAME_SEPERATOR + entry.getLocalName();
				fileList.remove(oldKey);
				// update the entry
				entry.setFileHash(fileHash);
				entry.setLocalName(localFilename);
				fileList.put(newFilename , entry);
			}
		}


		// write the file to disk
		FileOutputStream outputStream;
		try {
			outputStream = new FileOutputStream(BULib + localFilename);
			outputStream.write(inputFileInBytes);
			outputStream.close();
		} catch (FileNotFoundException e) {
			ServerUtils.sendInternErrMsg(exchange);
			e.printStackTrace();
			System.out.println("Error: file not found.");
		} catch (IOException e) {
			ServerUtils.sendInternErrMsg(exchange);
			e.printStackTrace();
			System.out.println("IO Error.");
			return;
		}

		// update listing file on disk
		if (updateListing()){
			// send back ok to the client
			ServerUtils.sendOKMsg(exchange);
		}
		else {
			ServerUtils.sendInternErrMsg(exchange);
		}
	}


	public void deleteSnapshot(HttpExchange exchange, String userName, int snapshotID){
		//now go over all user files and look for snapshotID, delete it from snapshots[]
		boolean snapshotExists = false;
		for (ListingEntry entry : fileList.values()){
			if (entry.getOwner().equals(userName) && entry.isInSnapshot(snapshotID)){
				snapshotExists = true;
				break;
			}
		}
		if(snapshotExists && snapshotID != 0){
		
			if(deleteSnapshotPointers(userName, snapshotID)){
				if (updateListing()){
					// send back ok to the client
					ServerUtils.sendOKMsg(exchange);
				}
				else {
					ServerUtils.sendInternErrMsg(exchange);
				}
			}
		}
		else{
			ServerUtils.sendBadRequestMsg(exchange);
		}
	}

	/* a method to get the corresponding local file's name according to it's hash
	 * or null if the file is not listed
	 */
	public static String getLocalFileByHash(byte[] hash){
		if (hash == null){
			return null;
		}
		for (ListingEntry entry : fileList.values()){
			boolean equal = Utils.md256equal(entry.getFileHash(), hash);
			if (equal){
				return entry.getLocalName();
			}
		}
		return null;	//didn't find the hash in the listing
	}


	/* answers whether or not there is an entry with filename for owner with the
	 * mentioned snapshot 
	 */
	public static boolean filenameExists(String fileName, String owner, int snapshot){
		if (fileName == null || owner == null){
			return false;
		}

		for (ListingEntry entry : fileList.values()){
			if (entry.getFilename().equals(fileName) 
					&& entry.getOwner().equals(owner)
					&& entry.isInSnapshot(snapshot)){
				return true;
			}
		}		
		return false;
	}

	/* return the ListingEntry that has the filename ,owner and snapshot given
	 * or null if it does not exist
	 */
	public static ListingEntry getEntry(String filename, String owner, int snapshot){
		if (filename == null || owner == null){
			return null;
		}
		for (ListingEntry entry : fileList.values()){
			if (entry.getFilename().equals(filename) 
					&& entry.getOwner().equals(owner)
					&& entry.isInSnapshot(snapshot)){
				return entry;
			}
		}		
		return null;
	}

	//goes over all owner entries and adds sID snapshotID (adds only to working copies)
	public boolean createSnapshotPointers(String owner, int snapshotID){

		for (ListingEntry entry : fileList.values()){
			if (entry.getOwner().equals(owner) && entry.isInSnapshot(snapshotID)){
				return false;
			}
		}

		for (ListingEntry entry : fileList.values()){
			if (entry.getOwner().equals(owner) && entry.isInSnapshot(Constants.CURRENT_SNAPSHOT)){
				ServerUtils.addSnapshotPtrToEntry(entry,snapshotID);
				refList.addCount(entry.getLocalName());
			}
		}
		return true;
	}	

	//goes over all owner entries and deletes sID snapshotID (maybe deletion of entry required)
	public boolean deleteSnapshotPointers(String owner, int snapshotID){
		for (ListingEntry entry : fileList.values()){
			if (entry.getOwner().equals(owner)&& entry.isInSnapshot(snapshotID)){
				ServerUtils.removeSnapshotPtrFromEntry(entry,snapshotID);
				deleteEntryOnLastSnapshot(entry,false);
				refList.remCount(entry.getLocalName());
				// remove current snapshot mark from the old entry and remove ref-count
				deleteOnLastRef(entry.getLocalName());
			}

		}
		
		for (String fileNameToClean : fileListObjectsToClean) {
			fileList.remove(fileNameToClean);
		}
		fileListObjectsToClean.clear();
		return true;
	}

	public static boolean updateListing(){
		try {
			Listing.writeListing(BULib + ServerUtils.LISTING_FILE_NAME, listing);

		} catch (FileNotFoundException e) {
			e.printStackTrace();
			System.out.println("Error updating listing file on disk.");
			return false;
		}

		return true;
	}

	// checks if the filename is of a file without any references and deletes it if needed
	public static void deleteOnLastRef(String filename){
		if (refList.getCount(filename) == 0){
			//String prevLocalFilename = AlibabaServer.fileList.get(filename).getLocalName();
			ServerUtils.deleteFile(BULib + filename);
			refList.removeEntry(filename);
		}
	}

	public static void deleteEntryOnLastSnapshot(ListingEntry entry, boolean deleteFromFileList){
		if (entry.getSnapshotVersion() == null){
			String fileToRemove = entry.getOwner() 
			+ Constants.FILENAME_SEPERATOR + entry.getFilename()
			+ Constants.FILENAME_SEPERATOR + entry.getLocalName();

			if(deleteFromFileList){
				fileList.remove(fileToRemove);
			}
			else{
				fileListObjectsToClean.add(fileToRemove);
			}
		}
		return;
	}


	//returns the ref-count for a file named filename
	public static int getCount(String filename){
		String localFilename = fileList.get(filename).getLocalName();
		return refList.getCount (localFilename);
	}

	/* creates the byte[] of the list we send back on check.
	 * format: <filename1> , <Base64(Hash(filename1))>\n<filename2> , <Base64(Hash(filename2))>\n...
	 */
	public static byte[] createCheckResponse(List<ListingEntry> entryList, String owner) throws Exception {
		StringBuffer body = new StringBuffer();
		for (ListingEntry entry : entryList)
		{
			// get local filename
			String localPath = BULib + entry.getLocalName();

			// get file hash
			byte[] file = Utils.getBytesFromFile(localPath);
			byte[] fileHash = Utils.getSHA256hash(file);

			// check the file - zero the hash field on inconsistency
			boolean match = Utils.md256equal(fileHash, entry.getFileHash());
			if (!match) {		//signatures don't match
				byte[] blank = new byte[Constants.SIGNATURE_LEN];		
				entry.setFileHash(blank);		//zero the bad sign. in the listing
			}

			String hashBase64 = Base64.encodeBytes(entry.getFileHash(), Base64.URL_SAFE);
			body.append(entry.getFilename() + " , " + Arrays.toString(entry.getSnapshotVersion()) + " , " + hashBase64 + "\n");
		}
		return body.toString().getBytes();		
	}

	// send back the check response
	public static boolean sendCheckResponse(HttpExchange exchange, List<ListingEntry> ls, String userName){
		byte[] responseBody;
		try {
			responseBody = createCheckResponse(ls, userName);
			return ServerUtils.sendResponseBody(exchange, responseBody, "IO Error.");
		} catch (Exception e) {
			e.printStackTrace();
			ServerUtils.sendInternErrMsg(exchange);
			System.out.println("Error creating response");
			return false;
		}

	}
}
